/*
 * Created on Feb 15, 2008
 * Created by Paul Gardner
 * 
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License only.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

package com.aelitis.azureus.plugins.net.netstatus;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.DelayedEvent;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.core3.util.TimerEventPeriodic;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.ddb.DistributedDatabase;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseContact;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseException;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseKey;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseProgressListener;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseTransferHandler;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseTransferType;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseValue;

import com.aelitis.azureus.core.dht.DHT;
import com.aelitis.azureus.core.dht.transport.DHTTransportContact;
import com.aelitis.azureus.core.networkmanager.NetworkManager;
import com.aelitis.azureus.core.networkmanager.impl.tcp.TCPNetworkManager;
import com.aelitis.azureus.plugins.dht.DHTPlugin;
import com.aelitis.azureus.plugins.net.netstatus.NetStatusProtocolTesterBT.Session;

public class NetStatusProtocolTester implements DistributedDatabaseTransferHandler {
    private static final int REQUEST_HISTORY_MAX = 64;

    private static final int MAX_ACTIVE_TESTS = 3;
    private static final int MAX_TEST_TIME = 120 * 1000;

    private static final int TEST_TYPE_BT = 1;

    private static final int VERSION_INITIAL = 1;

    private static final int CURRENT_VERSION = VERSION_INITIAL;

    private static final int BT_MAX_SLAVES = 8;

    private NetStatusPlugin plugin;
    private PluginInterface plugin_interface;

    private DistributedDatabase ddb;

    private DHTPlugin dht_plugin;

    private testXferType transfer_type = new testXferType();

    private Map request_history = new LinkedHashMap(REQUEST_HISTORY_MAX, 0.75f, true) {
        protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > REQUEST_HISTORY_MAX;
        }
    };

    private List active_tests = new ArrayList();

    private TimerEventPeriodic timer_event = null;

    protected NetStatusProtocolTester(NetStatusPlugin _plugin, PluginInterface _plugin_interface) {
        plugin = _plugin;
        plugin_interface = _plugin_interface;

        try {
            PluginInterface dht_pi = plugin_interface.getPluginManager().getPluginInterfaceByClass(DHTPlugin.class);

            if (dht_pi != null) {

                dht_plugin = (DHTPlugin) dht_pi.getPlugin();
            }

            ddb = plugin_interface.getDistributedDatabase();

            if (ddb.isAvailable()) {

                ddb.addTransferHandler(transfer_type, this);

                log("DDB transfer type registered");

            } else {

                log("DDB transfer type not registered, DDB unavailable");
            }
        } catch (Throwable e) {

            log("DDB transfer type registration failed", e);
        }
    }

    public NetStatusProtocolTesterBT runTest(final NetStatusProtocolTesterListener listener) {
        return (runTest("", listener));
    }

    public NetStatusProtocolTesterBT runTest(String test_address, final NetStatusProtocolTesterListener listener) {
        final NetStatusProtocolTesterBT bt_tester = new NetStatusProtocolTesterBT(this, true);

        bt_tester.addListener(listener);

        bt_tester.start();

        addToActive(bt_tester);

        try {
            if (test_address.length() == 0) {

                DHT[] dhts = dht_plugin.getDHTs();

                DHT target_dht = null;

                int target_network = Constants.isCVSVersion() ? DHT.NW_CVS : DHT.NW_MAIN;

                for (int i = 0; i < dhts.length; i++) {

                    if (dhts[i].getTransport().getNetwork() == target_network) {

                        target_dht = dhts[i];

                        break;
                    }
                }

                if (target_dht == null) {

                    listener.logError("Distributed database unavailable");

                } else {

                    DHTTransportContact[] contacts = target_dht.getTransport().getReachableContacts();

                    final List f_contacts = new ArrayList(Arrays.asList(contacts));

                    final int[] ok = new int[] { 0 };

                    final int num_threads = Math.min(BT_MAX_SLAVES, contacts.length);

                    listener.log("Searching " + contacts.length + " contacts for " + num_threads + " test targets", false);

                    final AESemaphore sem = new AESemaphore("NetStatusProbe");

                    for (int i = 0; i < num_threads; i++) {

                        new AEThread2("NetStatusProbe", true) {
                            public void run() {
                                try {
                                    while (!bt_tester.isDestroyed()) {

                                        DHTTransportContact contact = null;

                                        synchronized (ok) {

                                            if (ok[0] < num_threads && f_contacts.size() > 0) {

                                                contact = (DHTTransportContact) f_contacts.remove(0);
                                            }
                                        }

                                        if (contact == null) {

                                            break;
                                        }

                                        try {
                                            DistributedDatabaseContact ddb_contact = ddb.importContact(contact.getAddress());

                                            if (tryTest(bt_tester, ddb_contact)) {

                                                synchronized (ok) {

                                                    ok[0]++;
                                                }
                                            }
                                        } catch (Throwable e) {

                                            listener.logError("Contact import for " + contact.getName() + " failed", e);
                                        }
                                    }
                                } finally {

                                    sem.release();
                                }
                            }
                        }.start();
                    }

                    for (int i = 0; i < num_threads; i++) {

                        sem.reserve();
                    }

                    listener.log("Searching complete, " + ok[0] + " targets found", false);
                }
            } else {

                String[] bits = test_address.split(":");

                if (bits.length != 2) {

                    log("Invalid address - use <host>:<port> ");

                    return (bt_tester);
                }

                InetSocketAddress address = new InetSocketAddress(bits[0].trim(), Integer.parseInt(bits[1].trim()));

                DistributedDatabaseContact contact = ddb.importContact(address);

                tryTest(bt_tester, contact);
            }
        } catch (Throwable e) {

            listener.logError("Test failed", e);

        } finally {

            bt_tester.addListener(new NetStatusProtocolTesterListener() {
                public void sessionAdded(Session session) {
                }

                public void complete(NetStatusProtocolTesterBT tester) {
                    removeFromActive(tester);
                }

                public void log(String str, boolean detailed) {
                }

                public void logError(String str) {
                }

                public void logError(String str, Throwable e) {
                }
            });

            bt_tester.setOutboundConnectionsComplete();

            new DelayedEvent("NetStatus:killer", 10 * 1000, new AERunnable() {
                public void runSupport() {
                    listener.log("Destroying tester", false);

                    bt_tester.destroy();
                }
            });
        }

        return (bt_tester);
    }

    protected boolean tryTest(NetStatusProtocolTesterBT bt_tester, DistributedDatabaseContact contact) {
        boolean use_crypto = NetworkManager.getCryptoRequired(NetworkManager.CRYPTO_OVERRIDE_NONE);

        log("Trying test to " + contact.getName());

        Map request = new HashMap();

        request.put("v", new Long(CURRENT_VERSION));

        request.put("t", new Long(TEST_TYPE_BT));

        request.put("h", bt_tester.getServerHash());

        request.put("c", new Long(use_crypto ? 1 : 0));

        Map reply = sendRequest(contact, request);

        byte[] server_hash = reply == null ? null : (byte[]) reply.get("h");

        if (server_hash != null) {

            log("    " + contact.getName() + " accepted test");

            bt_tester.testOutbound(adjustLoopback(contact.getAddress()), server_hash, use_crypto);

            return (true);

        } else {

            log("    " + contact.getName() + " declined test");

            return (false);
        }
    }

    protected InetSocketAddress adjustLoopback(InetSocketAddress address) {
        InetSocketAddress local = dht_plugin.getLocalAddress().getAddress();

        if (local.getAddress().getHostAddress().equals(address.getAddress().getHostAddress())) {

            return (new InetSocketAddress("127.0.0.1", address.getPort()));

        } else {

            return (address);
        }
    }

    protected Map sendRequest(DistributedDatabaseContact contact, Map request) {
        try {
            log("Sending DDB request to " + contact.getName() + " - " + request);

            DistributedDatabaseKey key = ddb.createKey(BEncoder.encode(request));

            DistributedDatabaseValue value = contact.read(new DistributedDatabaseProgressListener() {
                public void reportSize(long size) {
                }

                public void reportActivity(String str) {
                }

                public void reportCompleteness(int percent) {
                }
            }, transfer_type, key, 10000);

            if (value == null) {

                return (null);
            }

            Map reply = BDecoder.decode((byte[]) value.getValue(byte[].class));

            log("    received reply - " + reply);

            return (reply);

        } catch (Throwable e) {

            log("sendRequest failed", e);

            return (null);
        }
    }

    protected Map receiveRequest(InetSocketAddress originator, Map request) {
        Map reply = new HashMap();

        Long test_type = (Long) request.get("t");

        reply.put("v", new Long(CURRENT_VERSION));

        if (test_type != null) {

            if (test_type.intValue() == TEST_TYPE_BT) {

                TCPNetworkManager tcp_man = TCPNetworkManager.getSingleton();

                InetSocketAddress adjusted_originator = adjustLoopback(originator);

                boolean test = adjusted_originator.getAddress().isLoopbackAddress();

                if (test
                        || (tcp_man.isTCPListenerEnabled() && tcp_man.getTCPListeningPortNumber() == ddb.getLocalContact().getAddress().getPort() && SystemTime
                                .getCurrentTime()
                                - tcp_man.getLastIncomingNonLocalConnectionTime() <= 24 * 60 * 60 * 1000)) {

                    byte[] their_hash = (byte[]) request.get("h");

                    if (their_hash != null) {

                        NetStatusProtocolTesterBT bt_tester;

                        synchronized (active_tests) {

                            if (active_tests.size() > MAX_ACTIVE_TESTS) {

                                log("Too many active tests");

                                return (reply);

                            } else {

                                bt_tester = new NetStatusProtocolTesterBT(this, false);

                                bt_tester.start();

                                addToActive(bt_tester);
                            }
                        }

                        Long l_crypto = (Long) request.get("c");

                        boolean use_crypto = l_crypto != null && l_crypto.longValue() == 1;

                        bt_tester.testOutbound(adjusted_originator, their_hash, use_crypto);

                        reply.put("h", bt_tester.getServerHash());
                    }
                }
            }
        }

        return (reply);
    }

    protected void addToActive(NetStatusProtocolTesterBT tester) {
        synchronized (active_tests) {

            active_tests.add(tester);

            if (timer_event == null) {

                timer_event = SimpleTimer.addPeriodicEvent("NetStatusProtocolTester:timer", 30 * 1000, new TimerEventPerformer() {
                    public void perform(TimerEvent event) {
                        long now = SystemTime.getCurrentTime();

                        List to_remove = new ArrayList();

                        synchronized (active_tests) {

                            for (int i = 0; i < active_tests.size(); i++) {

                                NetStatusProtocolTesterBT tester = (NetStatusProtocolTesterBT) active_tests.get(i);

                                long start = tester.getStartTime(now);

                                if (now - start > MAX_TEST_TIME) {

                                    to_remove.add(tester);
                                }
                            }
                        }

                        for (int i = 0; i < to_remove.size(); i++) {

                            removeFromActive((NetStatusProtocolTesterBT) to_remove.get(i));
                        }
                    }
                });
            }
        }
    }

    protected void removeFromActive(NetStatusProtocolTesterBT tester) {
        tester.destroy();

        synchronized (active_tests) {

            active_tests.remove(tester);

            if (active_tests.size() == 0) {

                if (timer_event != null) {

                    timer_event.cancel();

                    timer_event = null;
                }
            }
        }
    }

    public DistributedDatabaseValue read(DistributedDatabaseContact contact, DistributedDatabaseTransferType type, DistributedDatabaseKey ddb_key)

    throws DistributedDatabaseException {
        Object o_key = ddb_key.getKey();

        try {
            byte[] key = (byte[]) o_key;

            HashWrapper hw = new HashWrapper(key);

            synchronized (request_history) {

                if (request_history.containsKey(hw)) {

                    return (null);
                }

                request_history.put(hw, "");
            }

            Map request = BDecoder.decode((byte[]) o_key);

            log("Received DDB request from " + contact.getName() + " - " + request);

            Map result = receiveRequest(contact.getAddress(), request);

            return (ddb.createValue(BEncoder.encode(result)));

        } catch (Throwable e) {

            log("DDB read failed", e);

            return (null);
        }
    }

    public DistributedDatabaseValue write(DistributedDatabaseContact contact, DistributedDatabaseTransferType type, DistributedDatabaseKey key,
            DistributedDatabaseValue value)

    throws DistributedDatabaseException {
        throw (new DistributedDatabaseException("not supported"));
    }

    public void log(String str) {
        plugin.log(str);
    }

    public void log(String str, Throwable e) {
        plugin.log(str, e);
    }

    protected class testXferType implements DistributedDatabaseTransferType {
    }
}
